Websocket 一、基础知识 1 通讯方式 
轮询:让客户端每隔一定时间向服务端发送一次请求。缺点:延迟、请求太多服务端压力大。
长轮询:客户端向服务端发送请求,服务器保持这个请求一定时间,一旦有数据到来就立即返回数据。否则保持一段间后返回没有新数据,此时客户端重新发送请求,保持循环。优点:数据的响应没有延迟。例如:WebQQ,Web微信等。
websocket:客户端和服务端创建连接以后,这个连接不会断开。那么就可以实现双向通信。
 
 
二、 轮询方式 1 简单案例 
前端定时向后端发送请求,后端发现数据更新后将新数据发送回去。
前端设定了一个index,来确保每次发送的数据都是新数据,不用重复发送数据。
 
<!DOCTYPE html > <html  lang ="en" > <head >     <meta  charset ="UTF-8" >      <title > Title</title >      <style >          .message {             height : 500px ;             border : 1px  solid #dddddd ;             width : 100% ;         }      </style > </head > <body > <script  src ="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js" > </script > <script >          function  sendMessage ( ){         var  text = $("#txt" ).val ();         $.ajax ({             url :'/send/msg/' ,             type : 'GET' ,             data : {                 text : text             }, success : function (res ){                 console .log ("请求发送成功" , res)             }         })     }     max_index = 0 ; 	          setInterval (function ( ){         $.ajax ({             url :'/get/msg/' ,             type : 'GET' ,             data : {                 index : max_index             },             dataType : "JSON" ,             success : function (dataDict ){                 max_index = dataDict.max_index ;                 var  dataArray = dataDict.data ;                 $.each (dataArray, function  (index, item ) {                     console .log (index, item);                     var  tag = $("<div>" );                     tag.text (item);                     $("#message" ).append (tag);                 })             }         })     }, 2000 ) </script > <div  class ="message"  id ="message" > </div > <div >     <input  type ="text"  id ="txt"  placeholder ="请输入" >      <input  type ="button"  value ="发送"  onclick ="sendMessage();" >  </div > </body > </html > 
 
from  django.http import  HttpResponse, JsonResponsefrom  django.shortcuts import  renderDB = [] def  home (request ):    return  render(request, 'home.html' ) def  send_msg (request ):    text = request.GET.get('text' )     DB.append(text)     return  HttpResponse("OK" ) def  get_msg (request ):    try :         index = int (request.GET.get("index" ))     except  TypeError:         index = 0      context = {         "data" : DB[index:],         "max_index" : len (DB),     }     return  JsonResponse(context) 
 
 
三、 长轮询方式 1 简单案例 
访问/home/ 显示的聊天室界面。同时每个用户创建一个队列。
点击发送内容,数据也可以发送到后台。同时扔到每个人的队列中。
递归获取消息,去自己的队列中获取数据,然后再界面上展示。
 
<!DOCTYPE html > <html  lang ="en" > <head >     <meta  charset ="UTF-8" >      <title > Title</title >      <style >          .message {             height : 500px ;             border : 1px  solid #dddddd ;             width : 100% ;         }      </style > </head > <body > <script  src ="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js" > </script > <script >          USER_UID  = "{{ uid }}" ;          function  sendMessage ( ){         var  text = $("#txt" ).val ();         $.ajax ({             url :'/send/msg/' ,             type : 'GET' ,             data : {                 text : text             }, success : function (res ){                 console .log ("请求发送成功" , res)             }         })     }     function  getMessage ( ){         $.ajax ({             url :'/get/msg/' ,             type : 'GET' ,             data : {                 uid : USER_UID              },             dataType : "JSON" ,             success : function (res ){                                  console .log (res);                 if (res.status ){                     var  tag = $("<div>" );                     tag.text (res.data );                     $("#message" ).append (tag);                 }                                  getMessage ();             }         })     }      	$(function ( ){         getMessage ();     }) </script > <div  class ="message"  id ="message" > </div > <div >     <input  type ="text"  id ="txt"  placeholder ="请输入" >      <input  type ="button"  value ="发送"  onclick ="sendMessage();" >  </div > </body > </html > 
 
import  queuefrom  django.http import  HttpResponse, JsonResponsefrom  django.shortcuts import  renderUSER_QUEUE = {} def  home (request ):    uid = request.GET.get('uid' )     USER_QUEUE[uid] = queue.Queue()     return  render(request, 'home.html' , {'uid' : uid}) def  send_msg (request ):    text = request.GET.get('text' )     for  uid, q in  USER_QUEUE.items():         q.put(text)     return  HttpResponse("OK" ) def  get_msg (request ):    uid = request.GET.get('uid' )     q = USER_QUEUE[uid]                    res = {'status' : True , 'data' : None }          try :         data = q.get(timeout=10 )         res['data' ] = data     except  queue.Empty as  e:         res['status' ] = False      return  JsonResponse(res) 
 
2 优化方案 
Q:服务端持有这个连接,压力是否会很大?
A:使用IO多复用+异步可以解决。
 
 
四、websocket方式 1 定义 
简单理解:web版的socket。
使用场景:想要服务端向客户端主动推送消息:
 
2 原理 
author::你真的了解WebSocket吗? - 武沛齐 
http协议:
websocket协议,是建立在http协议之上的。
连接,客户端发起 
握手(验证),客户端发送一个消息,后端接收到消息再做一些特殊处理并返回 
收发数据(加密) 
断开连接 
 
请求和响应的【握手】信息需要遵循规则:
从请求【握手】信息中提取 Sec-WebSocket-Key 
利用magic_string 和 Sec-WebSocket-Key 进行hmac1加密,再进行base64加密 
将加密结果响应给客户端 
 
验证时服务器的具体操作:
获得Sec-WebSocket-Key = mnwFxiOlctXFN/DeMt1Amg== 
v1 = "mnwFxiOlctXFN/DeMt1Amg==" + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 
res = base64(hmac1(v1)) 
返回res 
 
解密时的数据包:
取第2个字节的后7位(payload length):
值=127时,数据头2字节、8字节,后面字节(4字节 masking key + 数据)。 
值=128,数据头2字节、2字节,后面字节(4字节 masking key + 数据)。 
值<=125,数据头2字节,后面字节(4字节 masking key + 数据)。 
 
 
获得masking key,然后对数据解密
var  DECODED  = "" ;for (var  i = 0 ;i < ENCODE .length ;i++){    DECODED [i] = ENCODED [i] ^ MASK [i % 4 ]; } 
 
 
 
 
验证时客服端发送的数据
GET  /chatsocket  HTTP/1.1 Host :  127.0.0.1:8002Connection :  UpgradePragma :  no-cacheCache-Control :  no-cacheUpgrade :  websocketOrigin :  http://localhost:63342Sec-WebSocket-Version :  13Sec-WebSocket-Key :  mnwFxiOlctXFN/DeMt1Amg==Sec-WebSocket-Extensions :  permessage-deflate; client_max_window_bits... ... 
 
验证时服务端返回的数据
HTTP/1.1  101  Switching ProtocolsUpgrade:websocket Connection:Upgrade Sec-WebSocket-Accept :  res
 
 
五、websocket使用 1 Django中配置 
安装:pip install channels。注意版本问题,使用不了就降低版本(实测Django=4.2.6可以用Channels=3.0.5)。
案例:新建一个项目是ws_demo,新建一个app是app001。
需要新增两个文件:一个routing.py(相当于websocket的urls.py),一个consumers.py(相当于websocket的views.py)
在django中需要了解:
wsgi:是一套Python Web的接口标准协议/规范。django一般情况下都是wsgi。启动时是Starting development server at http://127.0.0.1:8080/ 
asgi:wsgi + 异步 + websocket。启动时是Starting ASGI/Channels version 3.0.3 development server at http://127.0.0.1:8080/ 
 
制作流程:
访问地址看到聊天室的页面,服务端发送http请求。 
让客户端主动向服务端发起websocket连接,服务端接收到连接后通过(握手)。 
 
注意:
无论是客户端还是服务端主动断开连接,还是用户关闭了页面,都会触发websocket_disconnect()函数。 
 
客户端代码是返回的首页模板,服务端代码是修改customers.py。
 
INSTALLED_APPS = [     'channels'  ] ASGI_APPLICATION = "<project_name>.asgi.application"  
 
import  osfrom  django.core.asgi import  get_asgi_applicationfrom  channels.routing import  ProtocolTypeRouter, URLRouterfrom  ws_demo import  routingos.environ.setdefault('DJANGO_SETTINGS_MODULE' , 'ws_demo.settings' ) application = ProtocolTypeRouter({     "http" : get_asgi_application(),     "websocket" : URLRouter(routing.websocket_urlpatterns), }) 
 
from  django.urls import  re_pathfrom  app001 import  consumerswebsocket_urlpatterns = [          re_path(r'room/(?P<group>\w+)/$' , consumers.ChatConsumer.as_asgi()), ] 
 
from  channels.generic.websocket import  WebsocketConsumerfrom  channels.exceptions import  StopConsumerclass  ChatConsumer (WebsocketConsumer ):    def  websocket_connect (self, message ):         """ 有客户端来向后端发送websocket连接请求时,自动触发 """          print ("哼哼啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊" )                  self.accept()     def  websocket_receive (self, message ):         """ 浏览器基于websocket向后端发送数据,自动触发接收消息 """          print (message)         self.send("不要回复不要回复" )                  self.close()     def  websocket_disconnect (self, message ):         """ 客户端与服务端断开连接时,自动触发 """                            raise  StopConsumer() 
 
2 客户端发消息 <div  class ="message"  id ="message" > </div > <div >     <input  type ="text"  id ="txt"  placeholder ="请输入" >      <input  type ="button"  value ="发送"  onclick ="sendMessage();" >  </div > <script >          socket = new  WebSocket ("ws://127.0.0.1:8000/room/123/" );     function  sendMessage ( ){         let  message = document .getElementById ("txt" ).value ;                  socket.send (message);     } </script > 
 
from  channels.generic.websocket import  WebsocketConsumerfrom  channels.exceptions import  StopConsumerclass  ChatConsumer (WebsocketConsumer ):    def  websocket_connect (self, message ):         self.accept()     def  websocket_receive (self, message ):                  text = message.get("text" , " " )         print ("get: "  + text)     def  websocket_disconnect (self, message ):         raise  StopConsumer() 
 
3 服务端发消息 <div  class ="message"  id ="message" > </div > <div >     <input  type ="text"  id ="txt"  placeholder ="请输入" >      <input  type ="button"  value ="发送"  onclick ="sendMessage();" >  </div > <script >          socket = new  WebSocket ("ws://127.0.0.1:8000/room/123/" );             	     socket.onopen  = function  (event ) {         let  tag = document .createElement ("div" );         tag.innerHTML  = "[i]连接成功" ;         document .getElementById ("message" ).appendChild (tag);     };               socket.onmessage  = function  (event ) {         console .log (event.data );     }; </script > 
 
class  ChatConsumer (WebsocketConsumer ):    def  websocket_connect (self, message ):         """ 有客户端来向后端发送websocket连接请求时,自动触发 """                   self.accept()         self.send("[!]World Hello!" )     def  websocket_receive (self, message ):         """ 浏览器基于websocket向后端发送数据,自动触发接收消息 """          text = message.get("text" , " " )         print ("get: "  + text)     def  websocket_disconnect (self, message ):         """ 客户端与服务端断开连接时,自动触发 """          raise  StopConsumer() 
 
4 综合案例 
注:该案例只是客户端和服务端一对一通信,还不能群聊。
 
<div  class ="message"  id ="message" > </div > <div >     <input  type ="text"  id ="txt"  placeholder ="请输入" >      <input  type ="button"  value ="发送"  onclick ="sendMessage();" >      <input  type ="button"  value ="关闭"  onclick ="closeConn();" >  </div > <script >          socket = new  WebSocket ("ws://127.0.0.1:8000/room/123/" );          socket.onopen  = function  (event ) {         let  tag = document .createElement ("div" );         tag.innerHTML  = "[i]连接成功" ;         document .getElementById ("message" ).appendChild (tag);     };     socket.onmessage  = function  (event ) {         let  tag = document .createElement ("div" );         tag.innerHTML  = event.data ;         document .getElementById ("message" ).appendChild (tag);     };          socket.onclose  = function (event ){         let  tag = document .createElement ("div" );         tag.innerHTML  = "[x]连接关闭" ;         document .getElementById ("message" ).appendChild (tag);     }     function  sendMessage ( ){         let  message = document .getElementById ("txt" ).value ;         socket.send (message);     }     function  closeConn ( ){         socket.close ();     } </script > 
 
from  channels.generic.websocket import  WebsocketConsumerfrom  channels.exceptions import  StopConsumerclass  ChatConsumer (WebsocketConsumer ):    def  websocket_connect (self, message ):         """ 有客户端来向后端发送websocket连接请求时,自动触发 """                   self.accept()         self.send("[!]World Hello!" )     def  websocket_receive (self, message ):         """ 浏览器基于websocket向后端发送数据,自动触发接收消息 """          text = message.get("text" , " " )         print ("get: "  + text)                  if  text == "关闭" :             self.close()                                       return          self.send("[!]收到了"  + text + "喵" )     def  websocket_disconnect (self, message ):         """ 客户端与服务端断开连接时,自动触发 """          print ("客户端主动断开了喵" )                  raise  StopConsumer() 
 
5 连接列表实现群聊 
在后端维护有个连接列表,有连接来就加入列表中,然后发送消息时遍历用户发送消息,用户离开时就移除列表。
优点:实现快。
缺点:效率低,功能不够强大。
 
<div  class ="message"  id ="message" > </div > <div >     <input  type ="text"  id ="txt"  placeholder ="请输入" >      <input  type ="button"  value ="发送"  onclick ="sendMessage();" >      <input  type ="button"  value ="关闭"  onclick ="closeConn();" >  </div > <script >          socket = new  WebSocket ("ws://127.0.0.1:8000/room/123/" );          socket.onopen  = function  (event ) {         let  tag = document .createElement ("div" );         tag.innerHTML  = "[i]连接成功" ;         document .getElementById ("message" ).appendChild (tag);     };     socket.onmessage  = function  (event ) {         let  tag = document .createElement ("div" );         tag.innerHTML  = event.data ;         document .getElementById ("message" ).appendChild (tag);     };          socket.onclose  = function (event ){         let  tag = document .createElement ("div" );         tag.innerHTML  = "[x]连接关闭" ;         document .getElementById ("message" ).appendChild (tag);     }     function  sendMessage ( ){         let  message = document .getElementById ("txt" ).value ;         socket.send (message);     }     function  closeConn ( ){         socket.close ();     } </script > 
 
from  channels.generic.websocket import  WebsocketConsumerfrom  channels.exceptions import  StopConsumerCONN_LIST = [] class  ChatConsumer (WebsocketConsumer ):    def  websocket_connect (self, message ):         """ 有客户端来向后端发送websocket连接请求时,自动触发 """                   self.accept()         CONN_LIST.append(self)         self.send("[!]World Hello!" )     def  websocket_receive (self, message ):         """ 浏览器基于websocket向后端发送数据,自动触发接收消息 """          text = message.get("text" , " " )         print ("get: "  + text)         for  conn in  CONN_LIST:             conn.send("[!]收到了"  + text + "喵" )     def  websocket_disconnect (self, message ):         """ 客户端与服务端断开连接时,自动触发 """          print ("客户端主动断开了喵" )         CONN_LIST.remove(self)                  raise  StopConsumer() 
 
6 Channel layers实现 6.1 配置 
author::django channels - 武沛齐 - 博客园 (cnblogs.com) 
如果使用redis作为内存需要安装:pip3 install channels-redis
 
CHANNEL_LAYERS = {     "default" : {         "BACKEND" : "channels.layer.InMemeoryCahnnelLayer" ,     } } 
 
CHANNEL_LAYERS = {     'default' : {         'BACKEND' : 'channels_redis.core.RedisChannelLayer' ,         'CONFIG' : {"hosts" : ["redis://10.211.55.25:6379/1" ],},     }, } 
 
6.2使用 
客户端发送时是发送http://127.0.0.1:8000/index/?qq=123,来进入不同的群。
服务器依照群号来发送消息。
 
<div  class ="message"  id ="message" > </div > <div >     <input  type ="text"  id ="txt"  placeholder ="请输入" >      <input  type ="button"  value ="发送"  onclick ="sendMessage();" >      <input  type ="button"  value ="关闭"  onclick ="closeConn();" >  </div > <script >          socket = new  WebSocket ("ws://127.0.0.1:8000/room/{{ qq_group_num }}/" );          socket.onopen  = function (event ) {         let  tag = document .createElement ("div" );         tag.innerHTML  = "[i]连接成功" ;         document .getElementById ("message" ).appendChild (tag);     };     socket.onmessage  = function (event ) {         let  tag = document .createElement ("div" );         tag.innerHTML  = event.data ;         document .getElementById ("message" ).appendChild (tag);     };          socket.onclose  = function (event ) {         let  tag = document .createElement ("div" );         tag.innerHTML  = "[x]连接关闭" ;         document .getElementById ("message" ).appendChild (tag);     }     function  sendMessage ( ) {         let  message = document .getElementById ("txt" ).value ;         socket.send (message);     }     function  closeConn ( ){         socket.close ();     } </script > 
 
from  django.urls import  pathfrom  app001 import  viewsurlpatterns = [          path('index/' , views.index) ] 
 
from  django.shortcuts import  renderdef  index (request ):    qq_group_num = request.GET.get("qq" , "" )     return  render(request, 'index.html' ,{"qq_group_num" : qq_group_num}) 
 
from  channels.generic.websocket import  WebsocketConsumerfrom  channels.exceptions import  StopConsumerfrom  asgiref.sync import  async_to_syncclass  ChatConsumer (WebsocketConsumer ):    def  websocket_connect (self, message ):         """ 有客户端来向后端发送websocket连接请求时,自动触发 """                   self.accept()                  group = self.scope['url_route' ]['kwargs' ].get("group" )                           async_to_sync(self.channel_layer.group_add)(group, self.channel_name)     def  websocket_receive (self, message ):         """ 浏览器基于websocket向后端发送数据,自动触发接收消息 """          group = self.scope['url_route' ]['kwargs' ].get("group" )                  dic = {"type" : "xx.oo" , "message" : message}                  async_to_sync(self.channel_layer.group_send)(group, dic)     def  xx_oo (self, event ):         text = event['message' ]['text' ]         self.send("[!]收到了"  + text + "喵" )     def  websocket_disconnect (self, message ):         """ 客户端与服务端断开连接时,自动触发 """          group = self.scope['url_route' ]['kwargs' ].get("group" )         async_to_sync(self.channel_layer.group_discard)(group, self.channel_name)                  raise  StopConsumer()